Adding groundwater pumping

Developed by R.A. Collenteur & M. Bakker

In this example notebook it is shown how to simulate the effect of a pumping well on the groundwater levels. We will first create a TFN model with the net recharge as the single stress used to explain the observed heads. Second, this model is extended to include the effect of a pumping well on the heads by adding another stress model. The simulated heads are compared and it can be clearly seen how the addition of the pumping well improves the simulation of the heads.

This example was also shown at the 2018 General Assembly of the European Geophysical Union:


In [1]:
import pandas as pd
import pastas as ps
import matplotlib.pyplot as plt
%matplotlib inline

ps.show_versions()


Python version: 3.7.6 | packaged by conda-forge | (default, Jan  7 2020, 22:05:27) 
[Clang 9.0.1 ]
Numpy version: 1.17.5
Scipy version: 1.4.1
Pandas version: 0.25.0
Pastas version: 0.14.0b

1. Read the time series from files

All time series for this example have been prepared as csv-files, which are read using the Pandas read_csv- method. The following time series are available:

  • heads in meters above the Dutch National Datum (NAP), irregular time steps
  • rain in m/d
  • Makkink reference evaporation in m/d
  • Pumping extraction rate in m$^3$/d. The pumping well stopped operating after 2012.

In [2]:
head = pd.read_csv("data_notebook_7/head_wellex.csv", 
                   index_col="Date", parse_dates=True, squeeze=True)
rain =  pd.read_csv("data_notebook_7/prec_wellex.csv", 
                    index_col="Date", parse_dates=True)
evap =  pd.read_csv("data_notebook_7/evap_wellex.csv", 
                    index_col="Date", parse_dates=True)
well =  pd.read_csv("data_notebook_7/well_wellex.csv", 
                    index_col="Date", parse_dates=True)

# Make a plot of all the time series
fig, ax = plt.subplots(4, 1, sharex=True, figsize=(10,5));
ax[0].plot(head, label=head.name, linestyle=" ", marker=".", markersize=2)
ax[0].legend()
ax[1].plot(rain, label="rain")
ax[1].legend()
ax[2].plot(evap, label="evap")
ax[2].legend()
ax[3].plot(well, label="well")
ax[3].legend()


/Applications/anaconda3/envs/py37_pastas/lib/python3.7/site-packages/pandas/plotting/_matplotlib/converter.py:102: FutureWarning: Using an implicitly registered datetime converter for a matplotlib plotting method. The converter was registered by pandas on import. Future versions of pandas will require you to explicitly register matplotlib converters.

To register the converters:
	>>> from pandas.plotting import register_matplotlib_converters
	>>> register_matplotlib_converters()
  warnings.warn(msg, FutureWarning)
Out[2]:
<matplotlib.legend.Legend at 0x121aed950>

2. Create a Pastas Model

A pastas Model is created. A constant and a noisemodel are automatically added. The effect of the net groundwater recharge $R(t)$ is simulated using the ps.RechargeModel stress model. Net recharge is calculated as $R(t) = P(t) - f * E(t)$ where $f$ is a parameter that is estimated and $P(t)$ and $E(t)$ are precipitation and reference evapotranspiration, respectively.


In [3]:
# Create the time series model
ml = ps.Model(head, name="groundwater head")

# Add the stres model for the net recharge
rm = ps.StressModel2([rain, evap], name="recharge", rfunc=ps.Gamma)
ml.add_stressmodel(rm)
ml.solve()
ml.plot()

# Let's store the simulated values to compare later
sim1 = ml.simulate()
res1 = ml.residuals()
n1 = ml.noise()


INFO: Cannot determine frequency of series  Head
WARNING: User-provided name 'groundwater head' contains illegal character  
INFO: Inferred frequency from time series  Prec: freq=D 
INFO: Inferred frequency from time series  Evap: freq=D 
Model Results groundwater he        Fit Statistics
==================================================
nfev     25                     EVP          44.49
nobs     3869                   R2            0.09
noise    True                   RMSE          0.34
tmin     1995-01-14 00:00:00    AIC           9.51
tmax     2018-01-12 00:00:00    BIC          47.07
freq     D                      ___               
warmup   3650 days 00:00:00     ___               
solver   LeastSquares           ___               

Parameters (6 were optimized)
==================================================
                optimal   stderr     initial  vary
recharge_A   365.194118  ±10.93%  203.104730  True
recharge_n     1.406852   ±1.68%    1.000000  True
recharge_a    44.210741   ±9.59%   10.000000  True
recharge_f    -0.592277  ±12.97%   -1.000000  True
constant_d    15.273093   ±0.89%   15.975755  True
noise_alpha  358.234476  ±32.07%    1.000000  True

Parameter correlations |rho| > 0.5
==================================================
recharge_A recharge_a  0.87
           recharge_f  0.60
           constant_d -0.56
recharge_n recharge_a -0.62
recharge_f constant_d -0.55

Interpreting the results

As can be seen from the above plot, the observed heads show a clear rise whereas the simulated heads do not show this behaviour. The rise in the heads cannot be explained by an increased precipitation or a decreased evaporation over time, and it is likely another force is driving the heads upwards. Given the location of the well, we can hypothesize that the groundwater pumping caused a lowering of the heads in the beginning of the observations, which decreased when the pumping well was shut down. A next logical step is to add the effect of the pumping well and see if it improves the simulation of the head.

3. Add the effect of the pumping well

To simulate the effect of the pumping well a new stress model is added. The effect of the well is simulated using the ps.StressModel, which convoluted a stress with a response function. As a response function the ps.Hantush response function is used. The keyword-argument up=False is provided to tell the model this stress is supposed to have a lowering effect on the groundwater levels.


In [4]:
# Add the stress model for the pumping well
sm = ps.StressModel(well, rfunc=ps.Hantush, name="well", settings="well", up=False)
ml.add_stressmodel(sm)

# Solve the model and make a plot
ml.solve()
axes = ml.plots.decomposition(figsize=(10,8))
axes[0].plot(sim1) # Add the previously simulated values to the plot


INFO: Inferred frequency from time series  Well: freq=D 
Model Results groundwater he        Fit Statistics
==================================================
nfev     47                     EVP          75.30
nobs     3869                   R2            0.75
noise    True                   RMSE          0.18
tmin     1995-01-14 00:00:00    AIC          15.52
tmax     2018-01-12 00:00:00    BIC          71.87
freq     D                      ___               
warmup   3650 days 00:00:00     ___               
solver   LeastSquares           ___               

Parameters (9 were optimized)
==================================================
                optimal   stderr     initial  vary
recharge_A   534.262601   ±9.64%  203.104730  True
recharge_n     1.368284   ±1.55%    1.000000  True
recharge_a    65.041657   ±8.61%   10.000000  True
recharge_f    -0.551603  ±10.61%   -1.000000  True
well_A        -0.000098  ±11.81%   -0.000338  True
well_rho       1.554451  ±56.19%    1.000000  True
well_cS      114.641547  ±72.23%  100.000000  True
constant_d    15.457329   ±0.80%   15.975755  True
noise_alpha  104.548823  ±18.24%    1.000000  True

Parameter correlations |rho| > 0.5
==================================================
recharge_A recharge_a  0.83
           recharge_f  0.51
           constant_d -0.79
recharge_n recharge_a -0.58
recharge_a constant_d -0.58
recharge_f constant_d -0.72
well_A     constant_d -0.50
well_rho   well_cS    -0.95
Out[4]:
[<matplotlib.lines.Line2D at 0x1220d8b50>]

Interpreting the results

The addition of the pumping well to simulate the heads clearly improved the fit with the observed heads. It can also be seen how the pumping well stops contributing to the lowering of the head after ~2014, indicating the pumping effect of the well has dampened out. The period it takes before the historic pumping has no effect anymore can be approximated by the length of the response function for the well (e.g., len(ml.get_step_response("well"))).

4. Analyzing the residuals

The difference between the model with and without the pumping becomes even more clear when analyzing the model residuals. The residuals of the model without the well show a clear upward trend, whereas the model with a model does not show this trend anymore.


In [5]:
ml.residuals().plot(figsize=(10, 4))
res1.plot()
plt.legend(["Model with well", "Model without well"])


Out[5]:
<matplotlib.legend.Legend at 0x122954450>

In [ ]: